第16章 网络游戏初步

本章将介绍用love2d制作网络游戏的基本原理和实践操作。

网络游戏初步

基本的服务器和客户端

love内建了luasocket,它是个很容易使用的网络接口,具体内容请自行查阅lua官网关于luasocket的内容。
下面我们用实例来介绍一下luasocket的服务器和客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
local socket = require "socket"--由于是love内建,这里仅仅需要require这个接口即可。它提供了底层的网络接口功能。
local address, port = "localhost", 12345 --定义地址和端口
local entity = tostring(math.random(99999)) --我们要同步的对象,这里仅仅用一个随机数来代替,只不过强调的是唯一性。
local updaterate = 0.1 -- 同步速率,同步率越高,来往的流量越高,越精密
local world = {} -- 一个沙盒
local t = 0 --同步计时器
function love.load()
udp = socket.udp() --首先建立一个udp接口,关于udp和http之间的区别,自行百度。
udp:settimeout(0) --如果设置延迟的话,系统将阻塞,知道延迟结束,但是会卡住游戏,我们不想这样。
udp:setpeername(address, port) --设置连接服务器的地址和端口,注意,udp并不是真实连接,我们并不管发送消息之后是否能够抵达服务器。
--下面,我们开始制造一条信息,并发送了。这里的格式是:对象,行为,x,y。说到这个,有的内容跟存档差不多。
local dg = string.format("%s %s %d %d", entity, 'at', 320, 240) --制造一条消息。
udp:send(dg) -- 发送!
end
 
function love.update(deltatime)
 
t = t + deltatime --计时器叠加
  -- 我们使用这个计时器来控制网络更新速度,实际上,对于大多数游戏来讲,每秒10次的频率已经足够了,即使是网络游戏,而对于那些对延迟比较高的游戏,也不会要求更新频率高于30次每秒。
if t > updaterate then
local x, y = 0, 0
if love.keyboard.isDown('up') then y=y-(20*t) end
if love.keyboard.isDown('down') then y=y+(20*t) end
if love.keyboard.isDown('left') then x=x-(20*t) end
if love.keyboard.isDown('right') then x=x+(20*t) end
  --上面代码实现了一个简单移动。注意为什么是t而不是dt?因为我们使用了差速更新,需要用积累的数值。
  --接下来我们把位移的消息告诉给服务器
local dg = string.format("%s %s %f %f", entity, 'move', x, y)
udp:send(dg)
-- 然后我们发送一个同步命令。
local dg = string.format("%s %s $", entity, 'update')
udp:send(dg)
t=t-updaterate --重置计时器
end
 
repeat
--这里使用重复,是因为我们至少需要得到一个消息,所以要不停的监听。
data, msg = udp:receive()
if data then --这判断是否有消息进入,如果有就在msg中了。
  --就像存档一样,我们需要对匹配模式比较了解才能方便的解析命令。
local ent, cmd, parms = data:match("^(%S*) (%S*) (.*)")
if cmd == 'at' then --如果命令是定位,那么解析出 x,y
local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$")
assert(x and y) -- 这里只是看一眼数据是否合格而已。
x, y = tonumber(x), tonumber(y) -- 我们string解析出来的当然是字符串,而位置是数值。
world[ent] = {x=x, y=y} --我们沙盒中有了一个对象,名字是随机数,包含x,y两个属性。
else
--如果发现了不能识别的命令,最好还是查阅log,因为肯定有错误。
print("unrecognised command:", cmd)
end
--如果消息为否,说明有错误,而错误信息在msg中,因为我们设置了延迟为0,所以,得到的错误基本都是timeout,我们希望得到除了timeout外的其他错误。
elseif msg ~= 'timeout' then
error("Network error: "..tostring(msg))
end
until not data
end
function love.draw()
for k, v in pairs(world) do
love.graphics.print(k, v.x, v.y) --世界沙盒了,你画圆也可以。
end
end

服务器的代码相比客户端更为简单些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
local socket = require "socket"
local udp = socket.udp()
udp:settimeout(0)
udp:setsockname('*', 12345) --与客户端不同,这里的地址当然是本地了,端口得让客户端找得到。
local world = {} -- 同样世界沙盒
 
local data, msg_or_ip, port_or_nil --因为我们不想在一个快速循环里面放很多的局域变量,因此都写在外面了。
local entity, cmd, parms
local running = true --服务器开始的开关,额,实际上我就是想让它持续下去。
while running do
--开始我们的无限循环吧,你可以在设置里把graphics关掉,这样就没有窗体了。
data, msg_or_ip, port_or_nil = udp:receivefrom() --from可以让你得到额外的ip,便于我们区分数据来源。
--如果得到数据,那么返回msg,port否则只有ip
if data then
entity, cmd, parms = data:match("^(%S*) (%S*) (.*)")
匹配模式,不管啦。
if cmd == 'move' then
local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$")
assert(x and y)
x, y = tonumber(x), tonumber(y) --上面与客户端是一样的。
local ent = world[entity] or {x=0, y=0} --在服务器的沙盒里找对象,没找到就新建一个。
world[entity] = {x=ent.x+x, y=ent.y+y} --移动公式
elseif cmd == 'at' then
local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$")
assert(x and y)
x, y = tonumber(x), tonumber(y)
world[entity] = {x=x, y=y} --这个对于服务器来讲相当于生成+定位。
elseif cmd == 'update' then
for k, v in pairs(world) do --同步服务器沙盒到客户端沙盒。
udp:sendto(string.format("%s %s %d %d", k, 'at', v.x, v.y), msg_or_ip, port_or_nil)
end
elseif cmd == 'quit' then --关闭服务器? 你不想用客户端来直接这么做吧,哈哈。
running = false;
else
print("unrecognised command:", cmd) --打印未知命令,肯定有错误咯。
end
elseif msg_or_ip ~= 'timeout' then
error("Unknown network error: "..tostring(msg)) --其他错误
end
 
socket.sleep(0.01) --这个函数能让我们喘口气。。。不至于卡死了。
end

常用的网络库

grease love的作者的一个库(以前叫做lube),支持udp http enet,所以用着肯定没问题,稍微一点点复杂。
sock 这个库要求额外另个第三方库bister和enet(enet是udp的一个扩充),他把比较烦人的信息传递和解读部分打包了,而且用bister压缩,所以用起来比较方便。支持任意格式的数据。